/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of the CometMI Project.
 */
package org.cometmi.gwt.shared;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.cometmi.gwt.shared.events.AttributesCallback;
import org.cometmi.gwt.shared.events.BindCallback;
import org.cometmi.gwt.shared.request.Call;
import org.cometmi.gwt.shared.request.ConstructorCall;
import org.cometmi.gwt.shared.request.MethodCall;
import org.cometmi.gwt.shared.request.TrCommitCall;
import org.cometmi.gwt.shared.request.TrConstructorCall;
import org.cometmi.gwt.shared.request.TrFactoryCall;
import org.cometmi.gwt.shared.request.TrMethodCall;
import org.cometmi.gwt.shared.request.ptypes.Param;
import org.cometmi.gwt.shared.request.var.ObjectVar;
import org.cometmi.gwt.shared.request.var.ReturnVar;
import org.cometmi.gwt.shared.util.ObjectWrapper;
import org.cometmi.gwt.shared.util.WrapperFactory;

/**
 * Interface which enables to access functionalities available for a queue.
 * 
 * A queue can be created or reserved by an user, by providing valid
 * certificate; a queue can be reserved only in case it has no defined owner.
 * Once a queue has defined an owner only he/she can bind it, listening for
 * method invocations.
 * 
 * @author marco.tamburelli@gmail.com
 */
public abstract class MiQueue extends MiQueuePool
{
	protected boolean connected;
	protected boolean requestDisconnect;

	protected WrapperFactory wrapperFactory;

	private HashMap<String, ObjectWrapper> objectMap;
	private HashMap<String, HashMap<String, Object>> transactionsTable;

	protected MiQueue()
	{
		this.objectMap = new HashMap<String, ObjectWrapper>();
		this.transactionsTable = new HashMap<String, HashMap<String, Object>>();

		/* No connection and no disconnection request at the the creation */
		this.requestDisconnect = false;
		this.connected = false;
	}

	/**
	 * Binds an object to its id, and listen for its method invocation.
	 * 
	 * @param objectId The designed id of a provided object. An object can be
	 *        bound only if it implements {@link MiObject}.
	 * @param obj An object.
	 * @param callback The callback function receiving first bind event, can be
	 *        <code>null</code>.
	 */
	public void bind(String objectId, MiObject obj, BindCallback callback)
	{
		ObjectWrapper wrapper = this.wrapperFactory.createWrapper(obj.getClass());
		wrapper.setInstance(obj);

		this.objectMap.put(objectId, wrapper);

		bind(callback);
	}

	/**
	 * Listens for method invocations, and executes any method of already bound
	 * or created objects.
	 * 
	 * @param callback The callback function receiving first bind event, can be
	 *        <code>null</code>.
	 */
	public abstract void bind(BindCallback callback);

	/**
	 * This method unbind the queue, stopping to poll, and if the "discard" flag
	 * is set to <code>true</code> the queue will be discarder.
	 * 
	 * @param callback The callback function receiving first bind event, can be
	 *        <code>null</code>.
	 * @param discard When set to <code>true</code> teh queue will be discarded
	 *        firing the proper event on server.
	 */
	public abstract void unbind(BindCallback callback, boolean discard);

	/**
	 * This method unbind the queue, stopping to poll.
	 * 
	 * @param callback The callback function receiving first bind event, can be
	 *        <code>null</code>.
	 */
	public void unbind(BindCallback callback)
	{
		unbind(callback, false);
	}

	protected abstract void onResponse(List<Call> calls) throws Exception;

	protected void manageResponse(List<Call> calls) throws Exception
	{
		for (Call call : calls)
			if (call instanceof TrConstructorCall)
			{
				TrConstructorCall _c = (TrConstructorCall) call;

				getReturnValuesMap(_c.getTransactionId());

				ObjectWrapper wrapper = this.wrapperFactory.createWrapper(_c.getTargetClassName(),
						_c.getArgsSignature(), unwrapParameters(_c.getConstructorArgs()));

				this.objectMap.put(_c.getObjectId(), wrapper);
			}
			else if (call instanceof TrMethodCall)
			{
				TrMethodCall _c = (TrMethodCall) call;
				String transactionId = _c.getTransactionId();

				ObjectWrapper wrapper = this.objectMap.get(_c.getObjectId());
				Object returnValue = wrapper.invoke(_c.getMethod(), _c.getArgsSignature(),
						unwrapParameters(transactionId, _c.getArguments()));
				getReturnValuesMap(transactionId).put(_c.getReturnId(), returnValue);
			}
			else if (call instanceof TrFactoryCall)
			{
				TrFactoryCall _c = (TrFactoryCall) call;
				String transactionId = _c.getTransactionId();

				ObjectWrapper factoryWrapper = this.objectMap.get(_c.getObjectId());

				MiObject obj = (MiObject) factoryWrapper.invoke(_c.getFactoryMethod(), _c.getArgsSignature(),
						unwrapParameters(transactionId, _c.getFactoryMethodArgs()));

				ObjectWrapper productWrapper = this.wrapperFactory.createWrapper(obj.getClass());
				productWrapper.setInstance(obj);

				this.objectMap.put(_c.getCreatedObjectId(), productWrapper);
			}
			else if (call instanceof TrCommitCall)
				this.transactionsTable.remove(((TrCommitCall) call).getTransactionId());
			else if (call instanceof ConstructorCall)
			{
				ConstructorCall _c = (ConstructorCall) call;

				ObjectWrapper wrapper = this.wrapperFactory.createWrapper(_c.getTargetClassName(),
						_c.getArgsSignature(), unwrapParameters(_c.getConstructorArgs()));

				this.objectMap.put(_c.getObjectId(), wrapper);
			}
			else if (call instanceof MethodCall)
			{
				MethodCall _c = (MethodCall) call;

				ObjectWrapper wrapper = this.objectMap.get(_c.getObjectId());
				wrapper.invoke(_c.getMethod(), _c.getArgsSignature(), unwrapParameters(_c.getArguments()));
			}
	}

	private HashMap<String, Object> getReturnValuesMap(String transactionId)
	{
		if (!this.transactionsTable.containsKey(transactionId))
			this.transactionsTable.put(transactionId, new HashMap<String, Object>());

		return this.transactionsTable.get(transactionId);
	}

	/**
	 * Returns <code>true</code> if the queue is connected and is polling.
	 * 
	 * @return <code>true</code> or <code>false</code>.
	 */
	public boolean isConnected()
	{
		return this.connected;
	}

	protected void forceDisconnect()
	{
		this.connected = false;
		this.requestDisconnect = false;
	}

	/**
	 * When a disconnection request has been provided returns true. The result
	 * of this method doesn't tell if the connection has finished.
	 * 
	 * @return <code>true</code>
	 */
	public boolean isDisconnecting()
	{
		return this.requestDisconnect;
	}

	/**
	 * Notify that the queue should be disconnected on the next poll. It means
	 * that no further polling will be performed.
	 */
	protected void requestDisconnect()
	{
		this.requestDisconnect = true;
	}

	private Object[] unwrapParameters(List<Param<?>> params)
	{
		ArrayList<Object> cleaned = new ArrayList<Object>();

		for (Param<?> p : params)
		{
			Object v = p.getValue();

			if (v instanceof ObjectVar)
			{
				ObjectVar oVar = (ObjectVar) v;
				cleaned.add(this.objectMap.get(oVar.getObjectId()).getInstance());
			}
			else
				cleaned.add(v);
		}

		return cleaned.toArray();
	}

	private Object[] unwrapParameters(String transactionId, List<Param<?>> params)
	{
		ArrayList<Object> cleaned = new ArrayList<Object>();
		HashMap<String, Object> map = getReturnValuesMap(transactionId);

		for (Param<?> p : params)
		{
			Object v = p.getValue();

			if (v instanceof ReturnVar)
			{
				ReturnVar<?> rVar = (ReturnVar<?>) v;
				cleaned.add(map.get(rVar.getReturnId()));
			}
			else if (v instanceof ObjectVar)
			{
				ObjectVar oVar = (ObjectVar) v;
				cleaned.add(this.objectMap.get(oVar.getObjectId()).getInstance());
			}
			else
				cleaned.add(v);
		}

		return cleaned.toArray();
	}

	/**
	 * This method sets a queue attribute value by a key.
	 * 
	 * @param key The attribute key.
	 * @param value The attribute value
	 * @param callback The callback in case of something goes wrong.
	 */
	public abstract void setAttribute(String key, String value, AttributesCallback callback);

	/**
	 * Returns a previously inserted attribute value, or <code>null</code>. Note
	 * that if the attribute value is not available locally (only remotely) it
	 * will be returned via the {@link AttributesCallback#onSuccess(String)}
	 * method of the provided callback.
	 * 
	 * @param key The attribute key.
	 * @param callback The callback in case of something goes wrong.
	 * @return The attribute string value.
	 */
	public abstract String getAttribute(String key, AttributesCallback callback);

	/**
	 * Removes an attribute by its key, and returns it. Note that if the
	 * attribute value is not available locally (only remotely) it will be
	 * returned via the {@link AttributesCallback#onSuccess(String)} method of
	 * the provided callback.
	 * 
	 * @param key The attribute key.
	 * @param callback The callback in case of something goes wrong.
	 * @return The removes attribute string value.
	 */
	public abstract String removeAttribute(String key, AttributesCallback callback);
}
